"""Block Diagram Framework.

Terminal[key] is a shortcut to Terminal._dataDict[key]
Block[key] is a shortcut to Block._allTerminals[key]

"""

# TODO: this terminology: Terminal, Input, Output is confusing, specially when things have keys named "input", "output". I must find better names

import numpy as np
import sys
#sys.path.insert(0, '../../vera')
from misc import timedata

class Terminal(object):
    """Base for Input and Output.
    
    - Always belongs to a block
    - Indexing will access TimeData element.
    TODO: I think that the TimeData dictionary does not need to have TimeData only, this is a stupid restriction."""
    
    @property
    def fieldName(self):
        """Default data field name, for GetY()."""
        return self._fieldName
    @fieldName.setter
    def fieldName(self, x):
        self._fieldName = x
    
    def __init__(self, block, **kwargs):
        """Initialization of Terminal instance.
        
        Arguments:
            block -- parent block
            fieldName (keyword; optional)
        """
        self._block = block
        # Dictionary of TimeData
        self._dataDict = {}
        self._fieldName = kwargs.get("fieldname", "y")
    
    def __getitem__(self, key):
        """Indexing will access data elements."""
        return self._dataDict[key]
        
    def __setitem__(self, key, value):
        """Indexing will access data elements."""
        if self._dataDict.has_key(key):
            self._block._UnregisterTimeData(self._dataDict[key])
        self._dataDict[key] = value
        self._block._RegisterTimeData(value)

    def GetY(self, key=0):
        """Accesses fieldName attribute from TimeData specified by key."""
        return self[key].__getattribute__(self._fieldName)
        
    def GetGeneric(self, key=0):
        """Returns a timedata.Generic. See timedata.GetGeneric().
        
        ATTENTION: THIS DOES NOT COPY DATA, THEREFORE THE RESULT SHOULDN'T BE
        CHANGED!
        """
        return self[key].GetGeneric(self._fieldName)
        
        

    def SetY(self, value, key=0):
        """Sets fieldName attribute from TimeData specified by key."""
        return self[key].__setattr__(self._fieldName, value)

    
class Input(Terminal):
    """
    The "size" concept -- number of TimeData that need to be
    set until parent block is automatically pulsed. The pulse happens at
    __setitem__(). TODO: the problem here is that the actual *keys* of the
    data items within _dataDict is not checked at all. At some point I may implement
    a key names check on _dataDict items. Yeah, the dataDict should be more closely
    watched with possibly rejecting to add key if its name is not expected.
    """
    @property
    def flagFull(self):
        """Read-only property indicating whether the number of TimeData objects
        is at least equal to self._size."""
        return len(self._dataDict) >= self._size
        
    def __init__(self, block, **kwargs):
        """Initialization of Terminal instance.
        
        Arguments:
            block -- parent block
            size=1 (keyword; optional) -- See class docstring
        """
        Terminal.__init__(self, block, **kwargs)
        self._size = kwargs["size"] if kwargs.has_key("size") else 1
        
    def __setitem__(self, key, value):
        """Pulses block if number of TimeData equals _size."""
        Terminal.__setitem__(self, key, value)
        if self.flagFull:
            import logging
            self._block._Pulse()

class Output(Terminal):
    @property
    def inputs(self):
        """List of inputs connected to the Output."""
        return self._inputs
        
    def __init__(self, *args, **kwargs):
        Terminal.__init__(self, *args, **kwargs)
        self._inputs = []
        
    def Connect(self, dest):
        """Connects to an Input. Sends data immediately.
        TODO: is senting data immediatelly really correct???
        
        Output is male.
        
        Arguments:
            dest -- may be a Block, or Input instance."""
        if isinstance(dest, Block):
            # Assumes that the block has an Input called "input"
            self._inputs.append(dest["input"])
        elif isinstance(dest, Input):
            self._inputs.append(dest)
        else:
            raise RuntimeError("Destination of invalid class ``%s``" % \
                (dest.__class__.__name__))
        self._SendData(self._inputs[-1])
    
    def _SendData(self, input):
        """Fills Input data dictionary with all keys of self's own.
        
        _SendData and __setitem__ both transmit data to inputs; 
        The former transmits all data items to one Input only;
        The latter transmits one data item to all connected inputs."""
        for key, value in self._dataDict:
            input[key] = value
    
    def __setitem__(self, key, value):
        """Transmits value to all connections straight away."""
        Terminal.__setitem__(self, key, value)
        for input in self._inputs:
            input[key] = value


class Block(object):
    """Block base class.
    
    [] indexing accesses an input or output terminal.
    """
    def Connect(self, dest):
        """Calls Output's Connect() passing dest as parameter.
        
        Arguments:
            dest -- See Output.Connect()
        
        Makes sense only if the block has only one Output. Otherwise,
        which Output to connect?"""
        if len(self._outputs) != 1:
            raise RuntimeError("Cannot call Block.Connect() if number of Outputs is not 1 (%d)" % len(self._outputs))
        d = self._outputs
        d[d.keys()[0]].Connect(dest)
        
    def __getitem__(self, key):
        """Makes it possible to access inputs/outputs using indexing.
        
        For example: block["input"]; block["output"]
        """
        return self._allTerminals[key]
        
    def __setitem__(self, key, value):
        """Sets _allTerminals[key][0] with value."""
        self._allTerminals[key][0] = value

    # /\ Interface
    ###########################################################################
    # \/ Protected/private
    
    # To be overriden
    
    def _AddTerminals(self):
        """Inherit this to add Inputs and Outputs."""
        pass
        
    # Internals

    def __init__(self):
        self._inputs = {}
        self._outputs = {}
        # Contains simultaneously all keys from _inputs and _outputs
        self._allTerminals = {}
        self._AddTerminals()
        
    def _FindName(self, prefix, d):
        i = 0
        while True:
            name = "%s%1d" % (prefix, i)
            if not d.has_key(name):
                flag = True
                break
            i += 1
        self._lastName = name
        return name
    
    def _AddInput(self, name=None, inputClass=None):
        #TODO: make first automatic name be "input", then if called again to find automatic name, retrospectively rename first input to input0 and keep adding names
        """To be called from __init__()
        Arguments:
            name -- Input name. If not specified, a name "input<n>" will be
                    made up, where <n> has one digit only.
            inputClass --- defaults to Input.
        """
        if name is None:
            name = self._FindName("input", self._inputs)
        assert not self._inputs.has_key(name)
        assert not self._allTerminals.has_key(name)
        input = Input(self) if inputClass is None else inputClass(self)
        self._inputs[name] = input
        self._allTerminals[name] = input

    def _AddOutput(self, name=None):
        #TODO: make the analogous to what is commented in _AddInput
        """So far there is one type of Output only.
        Arguments:
            name -- Output name. If not specified, a name "output<n>" will be
                    made up, where <n> has one digit only.
        """
        if name is None:
            name = self._FindName("output", self._outputs)
        assert not self._outputs.has_key(name)
        assert not self._allTerminals.has_key(name)
        output = Output(self)
        self._outputs[name] = output
        self._allTerminals[name] = output

    def _Pulse(self):
        """Checks number of inputs and calls Use() or Train()
        
        TODO: training not implemented."""
        if all([x.flagFull for x in self._inputs.itervalues()]):
            # TODO: Here there could be a switch (either training or generating output)
            self._Use()
            
    # TODO: I think that this needs to be RegisterData() only
    def _RegisterTimeData(self, td):
        pass
    
    def _UnregisterTimeData(self, td):
        pass


class IBlock(Block):
    """Block with one input and no output."""
    def _AddTerminals(self):
        self._AddInput("input")


class OBlock(Block):
    """Block with one output and no input."""
    def _AddTerminals(self):
        self._AddOutput("output")
    

class IOBlock(Block):
    """Block with one input and one output."""
    def _AddTerminals(self):
        self._AddInput("input")
        self._AddOutput("output")

"""
Sample blocks

class Printer(IBlock):
    def __init__(self):
        IBlock.__init__(self)
        
    def _Use(self):
        print self["input"].GetY()
        
class Times(OBlock):
    def _AddTerminals(self):
        OBlock._AddTerminals(self)
        self._AddInput("a")
        self._AddInput("b")
        
    def _Use(self): 
        # Gotta get used
        # Shortcut to self._allTerminals["a"]._dataDict[0].FlatCopy()
        r = self["a"][0].FlatCopy()
        r.y = self["a"].GetY()*self["b"].GetY()
        self["output"][0] = r
"""
